// Copyright (C)2012 Johann Duscher. All rights reserved.
//
// This file is part of QwTAPI.
//
// QwTAPI is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// QwTAPI is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with QwTAPI.  If not, see <http://www.gnu.org/licenses/>.

#include "Connection.h"

#include <stdexcept>

#include "Line.h"


namespace qwtapi
{

Connection* Connection::instance()
{
    static Connection connection;

    return &connection;
}

void Connection::tapiCallback(DWORD hDevice, DWORD dwMsg, DWORD dwCallbackInstance, DWORD dwParam1, DWORD dwParam2, DWORD dwParam3)
{
    // Make sure event handling takes place in Qt's event handling thread.
    QMetaObject::invokeMethod(instance(), "onTAPIEvent", Qt::QueuedConnection,
        Q_ARG(DWORD, hDevice),
        Q_ARG(qwtapi::Message::Constant, static_cast<qwtapi::Message::Constant>(dwMsg)),
        Q_ARG(DWORD, dwCallbackInstance),
        Q_ARG(DWORD, dwParam1),
        Q_ARG(DWORD, dwParam2),
        Q_ARG(DWORD, dwParam3));
}

Connection::Connection(QObject *parent)
    : QObject(parent),
      m_hLineApp(NULL),
      m_lineListMutex(QMutex::Recursive),
      m_requestMapMutex(QMutex::Recursive)
{
    registerMetaTypes();
}

Connection::~Connection()
{
    shutdown();
}

LONG Connection::initialize(const QString& appName, DWORD apiVersion)
{
    if (NULL != m_hLineApp)
        throw std::runtime_error("No multiple instance!");

    LONG errCode = initLines(appName, apiVersion);
    if (errCode)
        return errCode;

    return errCode;
}

LONG Connection::shutdown()
{
    LONG errCode = 0;
    if (m_hLineApp)
    {
        errCode = lineShutdown(m_hLineApp);
        m_hLineApp = NULL;
    }

    QMutexLocker lock(&m_lineListMutex);
    foreach (Line* pLine, m_lineList)
        delete pLine;
    m_lineList.clear();

    return errCode;
}

LONG Connection::initLines(const QString& appName, DWORD apiVersion)
{
    LONG errCode = -1;

    DWORD numLines = 0;
    while (0 != errCode)
    {
        LINEINITIALIZEEXPARAMS initParams = {};
        initParams.dwTotalSize = sizeof(LINEINITIALIZEEXPARAMS);
        initParams.dwOptions = LINEINITIALIZEEXOPTION_USEHIDDENWINDOW;

        errCode = lineInitializeExW(&m_hLineApp, NULL, (LINECALLBACK)tapiCallback, appName.toStdWString().c_str(), &numLines, &apiVersion, &initParams);
        if (LINEERR_REINIT != errCode)
            break;
    }

    if (0 != errCode)
        return errCode;

    QMutexLocker lock(&m_lineListMutex);
    for (DWORD deviceId = 0; deviceId < numLines; deviceId++)
    {
        Line* pLine = new Line(this);
        if (0 == pLine->initialize(deviceId))
            m_lineList += pLine;
        else
            delete pLine;
    }

    return errCode;
}

// Open a line using LINEMAPPER and a set of calling parameters.
LONG Connection::openLine(Line** ppLine, CallPrivilege::Constants callPrivileges, MediaMode::Constants mediaModes, DWORD apiVersion, DWORD extVersion, LPLINECALLPARAMS const pCallParams)
{
    *ppLine = NULL;

    // Use lowest known version if we aren't passed one.
    if (apiVersion == 0)
        apiVersion = TAPI_VERSION_1_3;

    HLINE hLine = 0;
    LONG errCode = lineOpen(getLineAppHandle(), LINEMAPPER, &hLine, 
        apiVersion, extVersion, NULL, callPrivileges, 
        mediaModes, pCallParams);

    if (0 != errCode)
        return errCode;

    // We ere successfull, so we close the line and reopen it using our
    // line object in order to get notifications with the correct callback instance.
    QScopedArrayPointer<char> sapVarString(new char[sizeof(VARSTRING)+20]);
    LPVARSTRING lpVarString = (LPVARSTRING) sapVarString.data();
    lpVarString->dwTotalSize = sizeof(VARSTRING)+20;

    errCode = ::lineGetID(hLine, 0, NULL, LINECALLSELECT_LINE, lpVarString, L"tapi/line");
    lineClose(hLine);
    if (0 != errCode)
        return errCode;

    DWORD deviceId = *((LPDWORD)((LPCSTR)lpVarString + lpVarString->dwStringOffset));
    Line* pMyLine = getLineFromDeviceId(deviceId);
    if (!pMyLine)
        return LINEERR_LINEMAPPERFAILED;

    *ppLine = pMyLine;
    errCode = pMyLine->open(callPrivileges, mediaModes, apiVersion, extVersion, pCallParams);

    return errCode;
}

HLINEAPP Connection::getLineAppHandle() const
{
    return m_hLineApp;
}

DWORD Connection::getLineDeviceCount() const
{
    QMutexLocker lock(&m_lineListMutex);
    return m_lineList.size();
}

Connection::LineList Connection::findLines(MediaMode::Constants mediaModes) const
{
    LineList matchingLines;

    QMutexLocker lock(&m_lineListMutex);
    foreach (Line* pLine, m_lineList)
    {
        const LINEDEVCAPS* pCaps = pLine->getCapabilities();
        if (pCaps && pCaps->dwMediaModes & mediaModes)
            matchingLines += pLine;
    }

    return matchingLines;
}

Request* Connection::findRequestForId(LONG requestId) const
{
    QMutexLocker lock(&m_requestMapMutex);
    return m_requestMap.value(requestId, 0);
}

Request* Connection::createRequest(QObject* pSignalReceiver, const char* pMethod)
{
    Q_ASSERT(pSignalReceiver);
    Q_ASSERT(pMethod);

    Request* pRequest = new Request(this);
    pSignalReceiver->connect(pRequest, SIGNAL(requestComplete(const Request*)), pMethod);
    return pRequest;
}

void Connection::addRequest(Request* pRequest, Request::Type requestType, LONG requestId, int timeout)
{
    pRequest->initialize(requestType, requestId, timeout);

    QMutexLocker lock(&m_requestMapMutex);

    Request* pOldRequest = findRequestForId(requestId);
    if (pOldRequest)
        delete pOldRequest;

    m_requestMap.insert(requestId, pRequest);
    pRequest->startTimeoutTimer();
}

bool Connection::removeRequest(LONG requestId)
{
    QMutexLocker lock(&m_requestMapMutex);

    int numRemoved = m_requestMap.remove(requestId);

    return 0 != numRemoved;
}

void Connection::onTAPIEvent(DWORD hDevice, Message::Constant msg, DWORD dwCallbackInstance, DWORD dwParam1, DWORD dwParam2, DWORD dwParam3)
{
    qDebug() << Q_FUNC_INFO << hDevice << msg << dwCallbackInstance << dwParam1 << dwParam2 << dwParam3;
    switch (msg)
    {
    case Message::CLOSE:
        onLineClose(static_cast<HLINE>(hDevice));
        break;
    case Message::CREATE:
        onLineCreate(dwParam1);
        break;
    case Message::REMOVE:
        onLineRemove(dwParam1);
        break;
    case Message::REPLY:
        onLineReply(static_cast<LONG>(dwParam1), static_cast<LONG>(dwParam2));
        break;
        //case Message::REQUEST:
        //    onRequest(dwCBInstance, static_cast<RequestMode::Constant>(dwParam1), dwParam2, dwParam3);
        //    break;
    default:
        {
            Line* pLine = reinterpret_cast<Line*>(dwCallbackInstance);
            if (pLine)
                pLine->onLineEvent(hDevice, msg, dwParam1, dwParam2, dwParam3);
            else
                qDebug() << Q_FUNC_INFO << "UNHANDLED TAPI MESSAGE:" << hDevice << msg << dwCallbackInstance << dwParam1 << dwParam2 << dwParam3;
        }
    }
}

void Connection::onLineClose(DWORD deviceId)
{
    Line* pLine = getLineFromDeviceId(deviceId);
    Q_ASSERT(pLine);

    pLine->onForceClose();
}

void Connection::onLineCreate(DWORD deviceId)
{
    Q_ASSERT(NULL == getLineFromDeviceId(deviceId));

    // Add it to our array
    Line* pLine = 0;
    {
        QMutexLocker lock(&m_lineListMutex);

        pLine = new Line(this);
        if (0 == pLine->initialize(deviceId))
            m_lineList += pLine;
        else
        {
            delete pLine;
            pLine = 0;
        }
    }

    // Tell the line.
    if (pLine)
        pLine->onDynamicCreate();
}

void Connection::onLineRemove(DWORD deviceId)
{
    Line* pLine = getLineFromDeviceId(deviceId);
    Q_ASSERT(pLine);

    pLine->onDynamicRemove();
    // Original code marks pLine as removed, but does not remove it from the list.
    QMutexLocker lock(&m_lineListMutex);
    m_lineList.removeOne(pLine);
    delete pLine;
}

void Connection::onLineReply(LONG requestId, LONG result)
{
    QMutexLocker lock(&m_requestMapMutex);

    qDebug() << Q_FUNC_INFO << requestId << result;
    Request* pRequest = findRequestForId(requestId);
    Q_ASSERT_X(pRequest, Q_FUNC_INFO, "No request found. Maybe the reply was sent before the request was added to the request list.");
    pRequest->stopTimeoutTimer();
    pRequest->onRequestComplete(result);
}

Line* Connection::getLineFromDeviceId(DWORD deviceId) const
{
    QMutexLocker lock(&m_lineListMutex);
    
    foreach (Line* pLine, m_lineList)
        if (pLine->getDeviceId() == deviceId)
            return pLine;

    return NULL;
}

}
